Простые приложения на Kotlin
Простые приложения на Kotlin
Kotlin — это современный язык программирования, разработанный компанией JetBrains. Он полностью совместим с экосистемой Java, но предлагает более лаконичный синтаксис, безопасную работу с данными и продвинутые функции для работы с коллекциями. Язык особенно удобен для создания консольных утилит, скриптов автоматизации и простых приложений с графическим интерфейсом.
В этой главе рассматриваются примеры простых приложений, демонстрирующие базовые возможности языка: работу со строками, файлами, сетью, сериализацией данных и системными ресурсами. Каждый пример сопровождается разбором ключевых конструкций кода.
Генератор паролей
Генератор паролей демонстрирует работу со строками, массивами символов и генерацией случайных значений. Приложение создает пароль заданной длины, используя набор допустимых символов.
Код программы
import java.security.SecureRandom
fun generatePassword(length: Int): String {
val chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()_+-=[]{}|;:,.<>?"
val random = SecureRandom()
return (1..length)
.map { chars[random.nextInt(chars.length)] }
.joinToString("")
}
fun main() {
val passwordLength = 16
val password = generatePassword(passwordLength)
println("Сгенерированный пароль: $password")
}
Разбор кода
- SecureRandom: Класс из пакета
java.securityобеспечивает криптографически стойкую генерацию случайных чисел. Это предпочтительнее использования классаRandomдля создания паролей. - Коллекция символов: Строка
charsсодержит все допустимые символы для пароля. - Диапазон и маппинг: Оператор
(1..length)создает диапазон целых чисел от 1 до заданной длины. Методmapприменяет лямбда-выражение к каждому элементу диапазона, выбирая случайный символ из строкиchars. - joinToString: Метод объединяет список символов в одну строку без разделителей.
- Интерполяция строк: Конструкция
$passwordпозволяет вставлять значение переменной прямо в строку вывода.
Сортировщик текстового файла
Эта утилита читает текст из файла, сортирует слова по алфавиту и записывает результат в новый файл. Пример иллюстрирует работу с файловым вводом/выводом и коллекциями.
Код программы
import java.io.File
import java.util.Collections
fun sortWords(inputFile: String, outputFile: String) {
val file = File(inputFile)
if (!file.exists()) {
println("Файл не найден: $inputFile")
return
}
val content = file.readText()
val words = content.split("\\s+".toRegex()).filter { it.isNotEmpty() }
Collections.sort(words)
File(outputFile).writeText(words.joinToString("\n"))
println("Слова успешно сохранены в $outputFile")
}
fun main() {
sortWords("input.txt", "sorted_output.txt")
}
Разбор кода
- Чтение файла: Метод
readText()загружает всё содержимое файла в память как строку. - Разделение текста: Регулярное выражение
\\s+разделяет текст по пробельным символам (пробелы, табы, переносы строк). Фильтрацияfilter { it.isNotEmpty() }удаляет пустые элементы. - Сортировка: Метод
Collections.sort()сортирует список слов в лексикографическом порядке. - Запись файла: Метод
writeText()записывает отсортированный список слов в новый файл, разделяя их переносами строк.
Консольный калькулятор
Простой калькулятор выполняет арифметические операции (+, -, *, /) над двумя числами, введенными пользователем. Пример показывает обработку ввода, условные операторы и исключительные ситуации.
Код программы
fun calculate(a: Double, b: Double, operator: Char): Double? {
return when (operator) {
'+' -> a + b
'-' -> a - b
'*' -> a * b
'/' -> if (b != 0.0) a / b else null
else -> null
}
}
fun main() {
print("Введите первое число: ")
val num1 = readLine()?.toDoubleOrNull() ?: run {
println("Ошибка ввода числа"); return
}
print("Введите операцию (+, -, *, /): ")
val op = readLine()?.firstOrNull() ?: run {
println("Ошибка ввода операции"); return
}
print("Введите второе число: ")
val num2 = readLine()?.toDoubleOrNull() ?: run {
println("Ошибка ввода числа"); return
}
val result = calculate(num1, num2, op)
if (result != null) {
println("Результат: $result")
} else {
println("Неверная операция или деление на ноль.")
}
}
Разбор кода
- when: Конструкция
whenработает как расширенный операторswitch, позволяя сопоставлять значения оператора с соответствующими действиями. - Обработка ошибок: Метод
toDoubleOrNull()возвращаетnull, если строка не является числом. Оператор Elvis (?:) используется для обработки таких случаев и завершения выполнения функции. - Проверка деления: В случае деления проверяется, что делитель не равен нулю. Если условие нарушено, функция возвращает
null.
Трекер задач в JSON
Приложение управляет списком задач, сохраняя их в формате JSON. Пример демонстрирует использование библиотек для сериализации и десериализации данных, а также работу с файловой системой. Для работы потребуется библиотека kotlinx.serialization.
Структура данных
import kotlinx.serialization.Serializable
@Serializable
data class Task(
val id: Int,
val title: String,
val isCompleted: Boolean
)
Код программы
import kotlinx.serialization.json.Json
import kotlinx.serialization.Serializable
import java.io.File
@Serializable
data class Task(val id: Int, val title: String, val isCompleted: Boolean)
fun loadTasks(): List<Task> {
val file = File("tasks.json")
if (!file.exists()) return emptyList()
val jsonContent = file.readText()
return Json.decodeFromString<List<Task>>(jsonContent)
}
fun saveTasks(tasks: List<Task>) {
val file = File("tasks.json")
val jsonContent = Json.encodeToString(tasks)
file.writeText(jsonContent)
}
fun addTask(tasks: MutableList<Task>, title: String) {
val newId = tasks.maxOfOrNull { it.id }?.plus(1) ?: 1
tasks.add(Task(newId, title, false))
saveTasks(tasks)
}
fun main() {
val tasks = loadTasks().toMutableList()
// Добавление задачи
addTask(tasks, "Изучить Kotlin")
addTask(tasks, "Написать тесты")
// Вывод списка
println("Список задач:")
tasks.forEach { task ->
val status = if (task.isCompleted) "[x]" else "[ ]"
println("$status ${task.id}. ${task.title}")
}
}
Разбор кода
- Аннотация @Serializable: Указывает классу, что его экземпляры могут быть преобразованы в формат JSON.
- Json.encodeToString / decodeFromString: Методы библиотеки
kotlinx.serializationпреобразуют объекты в строки JSON и обратно. - Работа с файлами: Функции
loadTasksиsaveTasksобеспечивают сохранение состояния приложения между запусками. - Генерация ID: Метод
maxOfOrNullнаходит максимальный ID среди существующих задач, чтобы присвоить новому уникальный идентификатор.
Простой HTTP-сервер и клиент
Kotlin позволяет создавать простые сетевые приложения с использованием встроенных классов HttpURLConnection или сторонних библиотек. Ниже приведен пример сервера на основе стандартной библиотеки Java и клиента, отправляющего запрос.
Сервер
import java.net.ServerSocket
import java.net.Socket
import java.io.PrintWriter
import java.io.BufferedReader
import java.io.InputStreamReader
fun startServer(port: Int) {
ServerSocket(port).use { server ->
println("Сервер запущен на порту $port")
while (true) {
Socket().use { client ->
BufferedReader(InputStreamReader(client.inputStream)).use { reader ->
PrintWriter(client.outputStream, true).use { writer ->
val request = reader.readLine()
println("Получен запрос: $request")
val response = "HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\n\r\nHello from Kotlin Server!"
writer.println(response)
}
}
}
}
}
}
fun main() {
startServer(8080)
}
Клиент
import java.net.HttpURLConnection
import java.net.URL
fun sendRequest(url: String) {
val connection = URL(url).openConnection() as HttpURLConnection
connection.requestMethod = "GET"
val responseCode = connection.responseCode
val inputStream = connection.inputStream
val reader = BufferedReader(InputStreamReader(inputStream))
var line: String?
val response = StringBuilder()
while (reader.readLine().also { line = it } != null) {
response.append(line)
}
println("Код ответа: $responseCode")
println("Ответ сервера: ${response.toString()}")
inputStream.close()
connection.disconnect()
}
fun main() {
sendRequest("http://localhost:8080")
}
Разбор кода
- ServerSocket: Создает серверный сокет, который слушает входящие соединения на указанном порту.
- Socket: Представляет соединение между клиентом и сервером.
- HttpURLConnection: Класс для отправки HTTP-запросов и получения ответов.
- Потоки ввода/вывода:
BufferedReaderиPrintWriterиспользуются для чтения и записи текстовых данных через сеть.
Отправитель HTTP-запросов
Утилита для отправки произвольных HTTP-запросов с возможностью указания метода, заголовков и тела запроса.
Код программы
import java.net.HttpURLConnection
import java.net.URL
import java.io.OutputStream
import java.nio.charset.StandardCharsets
fun sendHttpRequest(method: String, url: String, headers: Map<String, String>, body: String?) {
val connection = URL(url).openConnection() as HttpURLConnection
connection.requestMethod = method
headers.forEach { (key, value) ->
connection.setRequestProperty(key, value)
}
if (body != null && method in listOf("POST", "PUT")) {
connection.doOutput = true
OutputStream(connection.outputStream).use { out ->
out.write(body.toByteArray(StandardCharsets.UTF_8))
}
}
val responseCode = connection.responseCode
val inputStream = connection.inputStream
val reader = BufferedReader(InputStreamReader(inputStream))
val response = StringBuilder()
var line: String?
while (reader.readLine().also { line = it } != null) {
response.append(line)
}
println("Метод: $method")
println("URL: $url")
println("Код ответа: $responseCode")
println("Тело ответа: ${response.toString()}")
inputStream.close()
connection.disconnect()
}
fun main() {
val headers = mapOf(
"Content-Type" to "application/json",
"Accept" to "application/json"
)
val body = """{"name": "Timur", "role": "Developer"}"""
sendHttpRequest("POST", "https://jsonplaceholder.typicode.com/posts", headers, body)
}
Разбор кода
- Настройка заголовков: Цикл
forEachустанавливает пользовательские заголовки в запросе. - Отправка тела: При использовании методов POST или PUT тело запроса записывается в поток вывода.
- Чтение ответа: Данные считываются из потока ввода и собираются в строку для отображения.
Утилита для сканирования директорий
Программа выводит список всех файлов и поддиректорий в указанной папке рекурсивно.
Код программы
import java.io.File
fun scanDirectory(path: String, indent: Int = 0) {
val file = File(path)
if (!file.exists()) {
println("Директория не найдена: $path")
return
}
if (file.isDirectory) {
println("${" ".repeat(indent)}📁 ${file.name}/")
file.listFiles()?.forEach { subFile ->
scanDirectory(subFile.absolutePath, indent + 2)
}
} else {
println("${" ".repeat(indent)}📄 ${file.name} (${file.length()} байт)")
}
}
fun main() {
scanDirectory(".")
}
Разбор кода
- Рекурсия: Функция вызывает сама себя для каждой поддиректории, увеличивая уровень отступа.
- File.listFiles(): Возвращает массив файлов в текущей директории.
- Отступы: Строка
" ".repeat(indent)создает визуальный отступ для отображения структуры дерева.
Скрипт для создания резервного копирования файлов
Скрипт копирует файлы из одной директории в другую, создавая временную метку в имени папки назначения.
Код программы
import java.io.File
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
fun createBackup(sourceDir: String, backupDir: String) {
val source = File(sourceDir)
val timestamp = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd_HH-mm-ss"))
val targetDir = File("$backupDir/backup_$timestamp")
if (!source.exists() || !source.isDirectory) {
println("Источник не существует или не является директорией: $sourceDir")
return
}
targetDir.mkdirs()
source.listFiles()?.forEach { file ->
if (file.isFile) {
val targetFile = File(targetDir, file.name)
file.copyTo(targetFile, overwrite = true)
println("Скопировано: ${file.name}")
}
}
println("Резервное копирование завершено в $targetDir")
}
fun main() {
createBackup("./data", "./backups")
}
Разбор кода
- Дата и время: Класс
LocalDateTimeи форматтерDateTimeFormatterгенерируют уникальное имя для папки бэкапа. - Копирование файлов: Метод
copyToкопирует файл в новую директорию с возможностью перезаписи. - Создание директории: Метод
mkdirs()создает всю цепочку директорий, если они отсутствуют.
Мониторинг дискового пространства
Утилита отображает информацию о свободном и занятом месте на дисках системы.
Код программы
import java.io.File
fun checkDiskSpace(path: String) {
val root = File(path)
val totalSpace = root.totalSpace
val freeSpace = root.freeSpace
val usableSpace = root.usableSpace
val usedSpace = totalSpace - usableSpace
println("Диск: $path")
println("Всего места: ${(totalSpace / (1024 * 1024 * 1024)).toInt()} ГБ")
println("Свободно: ${(freeSpace / (1024 * 1024 * 1024)).toInt()} ГБ")
println("Использовано: ${(usedSpace / (1024 * 1024 * 1024)).toInt()} ГБ")
println("Занято: %.2f%%".format((usedSpace.toDouble() / totalSpace.toDouble()) * 100))
}
fun main() {
checkDiskSpace("/")
checkDiskSpace("C:/") // Для Windows
}
Разбор кода
- Свойства объекта File: Поля
totalSpace,freeSpace,usableSpaceсодержат информацию о пространстве диска в байтах. - Конвертация единиц: Деление на
1024 * 1024 * 1024переводит байты в гигабайты. - Форматирование вывода: Метод
formatпозволяет вывести процент занятости с двумя знаками после запятой.
Парсер URL и проверка доступности ресурса
Программа анализирует URL, извлекает компоненты (протокол, хост, путь) и проверяет доступность ресурса.
Код программы
import java.net.URL
import java.net.HttpURLConnection
fun parseAndCheckUrl(urlString: String) {
try {
val url = URL(urlString)
println("Протокол: ${url.protocol}")
println("Хост: ${url.host}")
println("Порт: ${url.port}")
println("Путь: ${url.path}")
println("Запрос: ${url.query}")
val connection = url.openConnection() as HttpURLConnection
connection.requestMethod = "HEAD"
connection.connectTimeout = 5000
connection.readTimeout = 5000
val responseCode = connection.responseCode
if (responseCode == HttpURLConnection.HTTP_OK) {
println("Статус: Доступен ($responseCode)")
} else {
println("Статус: Недоступен ($responseCode)")
}
connection.disconnect()
} catch (e: Exception) {
println("Ошибка: ${e.message}")
}
}
fun main() {
parseAndCheckUrl("https://example.com/path/to/resource?key=value")
}
Разбор кода
- Класс URL: Автоматически разбирает строку URL на компоненты.
- HEAD запрос: Используется для проверки доступности ресурса без загрузки всего контента.
- Обработка исключений: Блок
try-catchперехватывает ошибки сети или неверного формата URL.
Конвертер форматов дат
Утилита преобразует строковое представление даты в различные форматы.
Код программы
import java.time.LocalDate
import java.time.format.DateTimeFormatter
fun convertDate(dateString: String, inputFormat: String, outputFormats: List<String>) {
val inputFormatter = DateTimeFormatter.ofPattern(inputFormat)
val date = LocalDate.parse(dateString, inputFormatter)
outputFormats.forEach { format ->
val formatter = DateTimeFormatter.ofPattern(format)
val converted = date.format(formatter)
println("$format -> $converted")
}
}
fun main() {
val inputDate = "2025-11-01"
val formats = listOf("dd/MM/yyyy", "MMMM dd, yyyy", "yyyy.MM.dd")
convertDate(inputDate, "yyyy-MM-dd", formats)
}
Разбор кода
- LocalDate: Класс для работы с датами без учета времени.
- DateTimeFormatter: Определяет шаблоны для парсинга и форматирования дат.
- Параметризация: Список выходных форматов позволяет гибко задавать нужные варианты представления даты.
Утилита для просмотра запущенных процессов
Программа выводит список активных процессов операционной системы с указанием их имен и идентификаторов.
Код программы
import java.lang.ProcessBuilder
import java.io.BufferedReader
import java.io.InputStreamReader
fun listProcesses() {
val command = if (System.getProperty("os.name").contains("win")) {
arrayOf("cmd.exe", "/c", "tasklist")
} else {
arrayOf("ps", "-aux")
}
val processBuilder = ProcessBuilder(*command)
val process = processBuilder.start()
BufferedReader(InputStreamReader(process.inputStream)).use { reader ->
var line: String?
while (reader.readLine().also { line = it } != null) {
println(line)
}
}
process.waitFor()
}
fun main() {
listProcesses()
}
Разбор кода
- ProcessBuilder: Запускает внешнюю команду операционной системы.
- Определение ОС: Логика выбора команды зависит от названия операционной системы.
- Чтение вывода: Вывод процесса считывается построчно и выводится в консоль.
Характерный пример для Kotlin
Одной из ключевых особенностей Kotlin является работа с nullable типами и безопасность операций. Приведенный пример демонстрирует использование оператора ?. (safe call), elvis-оператора ?: и функции let для безопасной работы с потенциально пустыми значениями.
Код программы
data class User(val name: String?, val age: Int?)
fun getUserInfo(user: User?) {
user?.let { u ->
val displayName = u.name ?: "Неизвестный пользователь"
val ageDisplay = u.age?.let { age -> "$age лет" } ?: "Возраст не указан"
println("Имя: $displayName")
println("Возраст: $ageDisplay")
} ?: run {
println("Пользователь не найден")
}
}
fun main() {
val user1 = User("Алексей", 30)
val user2 = User(null, null)
val user3: User? = null
getUserInfo(user1)
println("---")
getUserInfo(user2)
println("---")
getUserInfo(user3)
}
Разбор кода
- Safe Call (
?.): Вызывает метод только если объект не равенnull. - Elvis Operator (
?:): Возвращает альтернативное значение, если левая часть равнаnull. - Функция
let: Позволяет выполнить блок кода с объектом, передавая его как параметр. - Блоки
run: Выполняются при отсутствии объекта или при необходимости выполнить действие по умолчанию.
Этот подход делает код более читаемым и предотвращает возникновение NullPointerException, что является одним из главных преимуществ Kotlin перед Java.